package com.xdb.sqlparser.dialect.mysql.parser;

import com.xdb.sqlparser.ast.SQLDataType;
import com.xdb.sqlparser.ast.SQLExpr;
import com.xdb.sqlparser.ast.SQLName;
import com.xdb.sqlparser.ast.SQLOrderBy;
import com.xdb.sqlparser.ast.SQLOrderingSpecification;
import com.xdb.sqlparser.ast.expr.SQLAggregateExpr;
import com.xdb.sqlparser.ast.expr.SQLBinaryExpr;
import com.xdb.sqlparser.ast.expr.SQLBinaryOpExpr;
import com.xdb.sqlparser.ast.expr.SQLBinaryOperator;
import com.xdb.sqlparser.ast.expr.SQLCharExpr;
import com.xdb.sqlparser.ast.expr.SQLHexExpr;
import com.xdb.sqlparser.ast.expr.SQLIdentifierExpr;
import com.xdb.sqlparser.ast.expr.SQLIntegerExpr;
import com.xdb.sqlparser.ast.expr.SQLMethodInvokeExpr;
import com.xdb.sqlparser.ast.expr.SQLUnaryExpr;
import com.xdb.sqlparser.ast.expr.SQLUnaryOperator;
import com.xdb.sqlparser.ast.expr.SQLVariantRefExpr;
import com.xdb.sqlparser.ast.statement.SQLAssignItem;
import com.xdb.sqlparser.ast.statement.SQLColumnDefinition;
import com.xdb.sqlparser.dialect.mysql.ast.MySqlPrimaryKey;
import com.xdb.sqlparser.dialect.mysql.ast.MySqlUnique;
import com.xdb.sqlparser.dialect.mysql.ast.MysqlForeignKey;
import com.xdb.sqlparser.dialect.mysql.ast.MysqlForeignKey.Match;
import com.xdb.sqlparser.dialect.mysql.ast.MysqlForeignKey.On;
import com.xdb.sqlparser.dialect.mysql.ast.MysqlForeignKey.Option;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlCharExpr;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlExtractExpr;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlIntervalExpr;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlIntervalUnit;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlMatchAgainstExpr;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlOutFileExpr;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlSelectGroupByExpr;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlUserName;
import com.xdb.sqlparser.dialect.mysql.ast.expr.MySqlMatchAgainstExpr.SearchModifier;
import com.xdb.sqlparser.dialect.mysql.ast.statement.MySqlSQLColumnDefinition;
import com.xdb.sqlparser.dialect.mysql.ast.statement.MySqlSelectQueryBlock;
import com.xdb.sqlparser.dialect.mysql.ast.statement.MySqlSelectQueryBlock.Limit;
import com.xdb.sqlparser.dialect.sql99.parser.SQL99ExprParser;
import com.xdb.sqlparser.dialect.sql99.parser.SQL99SelectParser;
import com.xdb.sqlparser.exception.ParserException;
import com.xdb.sqlparser.parser.SQLLexer;
import com.xdb.sqlparser.parser.SQLToken;
import com.xdb.util.JDBCConstants;

public class MySqlExprParser extends SQL99ExprParser {

	public static String[] AGGREGATE_FUNCTIONS = {"AVG", "COUNT", "GROUP_CONCAT", "MAX", "MIN", "STDDEV", "SUM"};

	public MySqlExprParser(SQLLexer lexer) {
		super(lexer);
		this.aggregateFunctions = AGGREGATE_FUNCTIONS;
	}

	public MySqlExprParser(String sql) {
		this(new MySqlLexer(sql));
		this.lexer.nextToken();
	}

	public SQLExpr relationalRest(SQLExpr expr) {
		if (identifierEquals("REGEXP")) {
			lexer.nextToken();
			SQLExpr rightExp = equality();

			rightExp = relationalRest(rightExp);

			return new SQLBinaryOpExpr(expr, SQLBinaryOperator.RegExp, rightExp, JDBCConstants.MYSQL);
		}

		return super.relationalRest(expr);
	}

	public SQLExpr multiplicativeRest(SQLExpr expr) {
		if (lexer.token() == SQLToken.IDENTIFIER && "MOD".equalsIgnoreCase(lexer.stringVal())) {
			lexer.nextToken();
			SQLExpr rightExp = primary();

			rightExp = relationalRest(rightExp);

			return new SQLBinaryOpExpr(expr, SQLBinaryOperator.Modulus, rightExp, JDBCConstants.MYSQL);
		}

		return super.multiplicativeRest(expr);
	}

	public SQLExpr notRationalRest(SQLExpr expr) {
		if (identifierEquals("REGEXP")) {
			lexer.nextToken();
			SQLExpr rightExp = primary();

			rightExp = relationalRest(rightExp);

			return new SQLBinaryOpExpr(expr, SQLBinaryOperator.NotRegExp, rightExp, JDBCConstants.MYSQL);
		}

		return super.notRationalRest(expr);
	}

	public SQLExpr primary() {
		final SQLToken tok = lexer.token();

		if (identifierEquals("outfile")) {
			lexer.nextToken();
			SQLExpr file = primary();
			SQLExpr expr = new MySqlOutFileExpr(file);

			return primaryRest(expr);

		}

		switch (tok) {
			case LITERAL_ALIAS :
				String aliasValue = lexer.stringVal();
				lexer.nextToken();
				return primaryRest(new SQLCharExpr(aliasValue));
			case VARIANT :
				SQLVariantRefExpr varRefExpr = new SQLVariantRefExpr(lexer.stringVal());
				lexer.nextToken();
				if (varRefExpr.getName().equalsIgnoreCase("@@global")) {
					accept(SQLToken.DOT);
					varRefExpr = new SQLVariantRefExpr(lexer.stringVal(), true);
					lexer.nextToken();
				} else if (varRefExpr.getName().equals("@") && lexer.token() == SQLToken.LITERAL_CHARS) {
					varRefExpr.setName("@'" + lexer.stringVal() + "'");
					lexer.nextToken();
				} else if (varRefExpr.getName().equals("@@") && lexer.token() == SQLToken.LITERAL_CHARS) {
					varRefExpr.setName("@@'" + lexer.stringVal() + "'");
					lexer.nextToken();
				}
				return primaryRest(varRefExpr);
			case VALUES :
				lexer.nextToken();
				if (lexer.token() != SQLToken.LPAREN) {
					throw new ParserException("syntax error, illegal values clause");
				}
				return this.methodRest(new SQLIdentifierExpr("VALUES"), true);
			case BINARY :
				lexer.nextToken();
				if (lexer.token() == SQLToken.COMMA || lexer.token() == SQLToken.SEMI || lexer.token() == SQLToken.EOF) {
					return new SQLIdentifierExpr("BINARY");
				} else {
					SQLUnaryExpr binaryExpr = new SQLUnaryExpr(SQLUnaryOperator.BINARY, expr());
					return primaryRest(binaryExpr);
				}
			case CACHE :
			case GROUP :
				lexer.nextToken();
				return primaryRest(new SQLIdentifierExpr(lexer.stringVal()));
			default :
				return super.primary();
		}

	}

	public final SQLExpr primaryRest(SQLExpr expr) {
		if (expr == null) {
			throw new IllegalArgumentException("expr");
		}

		if (lexer.token() == SQLToken.LITERAL_CHARS) {
			if (expr instanceof SQLIdentifierExpr) {
				SQLIdentifierExpr identExpr = (SQLIdentifierExpr) expr;
				String ident = identExpr.getName();

				if (ident.equalsIgnoreCase("x")) {
					String charValue = lexer.stringVal();
					lexer.nextToken();
					expr = new SQLHexExpr(charValue);

					return primaryRest(expr);
				} else if (ident.equalsIgnoreCase("b")) {
					String charValue = lexer.stringVal();
					lexer.nextToken();
					expr = new SQLBinaryExpr(charValue);

					return primaryRest(expr);
				} else if (ident.startsWith("_")) {
					String charValue = lexer.stringVal();
					lexer.nextToken();

					MySqlCharExpr mysqlCharExpr = new MySqlCharExpr(charValue);
					mysqlCharExpr.setCharset(identExpr.getName());
					if (identifierEquals("COLLATE")) {
						lexer.nextToken();

						String collate = lexer.stringVal();
						mysqlCharExpr.setCollate(collate);
						accept(SQLToken.IDENTIFIER);
					}

					expr = mysqlCharExpr;

					return primaryRest(expr);
				}
			} else if (expr instanceof SQLCharExpr) {
				SQLMethodInvokeExpr concat = new SQLMethodInvokeExpr("CONCAT");
				concat.addParameter(expr);
				do {
					String chars = lexer.stringVal();
					concat.addParameter(new SQLCharExpr(chars));
					lexer.nextToken();
				} while (lexer.token() == SQLToken.LITERAL_CHARS || lexer.token() == SQLToken.LITERAL_ALIAS);
				expr = concat;
			}
		} else if (lexer.token() == SQLToken.IDENTIFIER) {
			if (expr instanceof SQLHexExpr) {
				if ("USING".equalsIgnoreCase(lexer.stringVal())) {
					lexer.nextToken();
					if (lexer.token() != SQLToken.IDENTIFIER) {
						throw new ParserException("syntax error, illegal hex");
					}
					String charSet = lexer.stringVal();
					lexer.nextToken();
					expr.getAttributes().put("USING", charSet);

					return primaryRest(expr);
				}
			} else if ("COLLATE".equalsIgnoreCase(lexer.stringVal())) {
				lexer.nextToken();

				if (lexer.token() == SQLToken.EQ) {
					lexer.nextToken();
				}

				if (lexer.token() != SQLToken.IDENTIFIER) {
					throw new ParserException("syntax error");
				}

				String collate = lexer.stringVal();
				lexer.nextToken();

				SQLBinaryOpExpr binaryExpr = new SQLBinaryOpExpr(expr, SQLBinaryOperator.COLLATE, new SQLIdentifierExpr(collate), JDBCConstants.MYSQL);

				expr = binaryExpr;

				return primaryRest(expr);
			} else if (expr instanceof SQLVariantRefExpr) {
				if ("COLLATE".equalsIgnoreCase(lexer.stringVal())) {
					lexer.nextToken();

					if (lexer.token() != SQLToken.IDENTIFIER) {
						throw new ParserException("syntax error");
					}

					String collate = lexer.stringVal();
					lexer.nextToken();

					expr.putAttribute("COLLATE", collate);

					return primaryRest(expr);
				}
			} else if (expr instanceof SQLIntegerExpr) {
				SQLIntegerExpr intExpr = (SQLIntegerExpr) expr;
				String binaryString = lexer.stringVal();
				if (intExpr.getNumber().intValue() == 0 && binaryString.startsWith("b")) {
					lexer.nextToken();
					expr = new SQLBinaryExpr(binaryString.substring(1));

					return primaryRest(expr);
				}
			}
		}

		if (lexer.token() == SQLToken.LPAREN && expr instanceof SQLIdentifierExpr) {
			SQLIdentifierExpr identExpr = (SQLIdentifierExpr) expr;
			String ident = identExpr.getName();

			if ("EXTRACT".equalsIgnoreCase(ident)) {
				lexer.nextToken();

				if (lexer.token() != SQLToken.IDENTIFIER) {
					throw new ParserException("syntax error");
				}

				String unitVal = lexer.stringVal();
				MySqlIntervalUnit unit = MySqlIntervalUnit.valueOf(unitVal.toUpperCase());
				lexer.nextToken();

				accept(SQLToken.FROM);

				SQLExpr value = expr();

				MySqlExtractExpr extract = new MySqlExtractExpr();
				extract.setValue(value);
				extract.setUnit(unit);
				accept(SQLToken.RPAREN);

				expr = extract;

				return primaryRest(expr);
			} else if ("SUBSTRING".equalsIgnoreCase(ident)) {
				lexer.nextToken();
				SQLMethodInvokeExpr methodInvokeExpr = new SQLMethodInvokeExpr(ident);
				for (;;) {
					SQLExpr param = expr();
					methodInvokeExpr.addParameter(param);

					if (lexer.token() == SQLToken.COMMA) {
						lexer.nextToken();
						continue;
					} else if (lexer.token() == SQLToken.FROM) {
						lexer.nextToken();
						SQLExpr from = expr();
						methodInvokeExpr.addParameter(from);

						if (lexer.token() == SQLToken.FOR) {
							lexer.nextToken();
							SQLExpr forExpr = expr();
							methodInvokeExpr.addParameter(forExpr);
						}
						break;
					} else if (lexer.token() == SQLToken.RPAREN) {
						break;
					} else {
						throw new ParserException("syntax error");
					}
				}

				accept(SQLToken.RPAREN);
				expr = methodInvokeExpr;

				return primaryRest(expr);
			} else if ("TRIM".equalsIgnoreCase(ident)) {
				lexer.nextToken();
				SQLMethodInvokeExpr methodInvokeExpr = new SQLMethodInvokeExpr(ident);

				if (lexer.token() == SQLToken.IDENTIFIER) {
					String flagVal = lexer.stringVal();
					if ("LEADING".equalsIgnoreCase(flagVal)) {
						lexer.nextToken();
						methodInvokeExpr.getAttributes().put("TRIM_TYPE", "LEADING");
					} else if ("BOTH".equalsIgnoreCase(flagVal)) {
						lexer.nextToken();
						methodInvokeExpr.getAttributes().put("TRIM_TYPE", "BOTH");
					} else if ("TRAILING".equalsIgnoreCase(flagVal)) {
						lexer.nextToken();
						methodInvokeExpr.putAttribute("TRIM_TYPE", "TRAILING");
					}
				}

				SQLExpr param = expr();
				methodInvokeExpr.addParameter(param);

				if (lexer.token() == SQLToken.FROM) {
					lexer.nextToken();
					SQLExpr from = expr();
					methodInvokeExpr.putAttribute("FROM", from);
				}

				accept(SQLToken.RPAREN);
				expr = methodInvokeExpr;

				return primaryRest(expr);
			} else if ("MATCH".equalsIgnoreCase(ident)) {
				lexer.nextToken();
				MySqlMatchAgainstExpr matchAgainstExpr = new MySqlMatchAgainstExpr();

				if (lexer.token() == SQLToken.RPAREN) {
					lexer.nextToken();
				} else {
					exprList(matchAgainstExpr.getColumns(), matchAgainstExpr);
					accept(SQLToken.RPAREN);
				}

				acceptIdentifier("AGAINST");

				accept(SQLToken.LPAREN);
				SQLExpr against = primary();
				matchAgainstExpr.setAgainst(against);

				if (lexer.token() == SQLToken.IN) {
					lexer.nextToken();
					if (identifierEquals("NATURAL")) {
						lexer.nextToken();
						acceptIdentifier("LANGUAGE");
						acceptIdentifier("MODE");
						if (lexer.token() == SQLToken.WITH) {
							lexer.nextToken();
							acceptIdentifier("QUERY");
							acceptIdentifier("EXPANSION");
							matchAgainstExpr.setSearchModifier(SearchModifier.IN_NATURAL_LANGUAGE_MODE_WITH_QUERY_EXPANSION);
						} else {
							matchAgainstExpr.setSearchModifier(SearchModifier.IN_NATURAL_LANGUAGE_MODE);
						}
					} else if (identifierEquals("BOOLEAN")) {
						lexer.nextToken();
						acceptIdentifier("MODE");
						matchAgainstExpr.setSearchModifier(SearchModifier.IN_BOOLEAN_MODE);
					} else {
						throw new ParserException("TODO");
					}
				} else if (lexer.token() == SQLToken.WITH) {
					throw new ParserException("TODO");
				}

				accept(SQLToken.RPAREN);

				expr = matchAgainstExpr;

				return primaryRest(expr);
			} else if ("CONVERT".equalsIgnoreCase(ident)) {
				lexer.nextToken();
				SQLMethodInvokeExpr methodInvokeExpr = new SQLMethodInvokeExpr(ident);

				if (lexer.token() != SQLToken.RPAREN) {
					exprList(methodInvokeExpr.getParameters(), methodInvokeExpr);
				}

				if (identifierEquals("USING")) {
					lexer.nextToken();
					if (lexer.token() != SQLToken.IDENTIFIER) {
						throw new ParserException("syntax error");
					}
					String charset = lexer.stringVal();
					lexer.nextToken();
					methodInvokeExpr.putAttribute("USING", charset);
				}

				accept(SQLToken.RPAREN);

				expr = methodInvokeExpr;

				return primaryRest(expr);
			} else if ("POSITION".equalsIgnoreCase(ident)) {
				accept(SQLToken.LPAREN);
				SQLExpr subStr = this.primary();
				accept(SQLToken.IN);
				SQLExpr str = this.expr();
				accept(SQLToken.RPAREN);

				SQLMethodInvokeExpr locate = new SQLMethodInvokeExpr("LOCATE");
				locate.addParameter(subStr);
				locate.addParameter(str);

				expr = locate;
				return primaryRest(expr);
			}
		}

		if (lexer.token() == SQLToken.VARIANT && "@".equals(lexer.stringVal())) {
			lexer.nextToken();
			MySqlUserName userName = new MySqlUserName();
			if (expr instanceof SQLCharExpr) {
				userName.setUserName(((SQLCharExpr) expr).toString());
			} else {
				userName.setUserName(((SQLIdentifierExpr) expr).getName());
			}

			if (lexer.token() == SQLToken.LITERAL_CHARS) {
				userName.setHost("'" + lexer.stringVal() + "'");
			} else {
				userName.setHost(lexer.stringVal());
			}
			lexer.nextToken();
			return userName;
		}

		if (lexer.token() == SQLToken.ERROR) {
			throw new ParserException("syntax error, token: " + lexer.token() + " " + lexer.stringVal() + ", pos : " + lexer.pos());
		}

		return super.primaryRest(expr);
	}

	public SQL99SelectParser createSelectParser() {
		return new MySqlSelectParser(this);
	}

	protected SQLExpr parseInterval() {
		accept(SQLToken.INTERVAL);

		if (lexer.token() == SQLToken.LPAREN) {
			lexer.nextToken();

			SQLMethodInvokeExpr methodInvokeExpr = new SQLMethodInvokeExpr("INTERVAL");
			if (lexer.token() != SQLToken.RPAREN) {
				exprList(methodInvokeExpr.getParameters(), methodInvokeExpr);
			}

			accept(SQLToken.RPAREN);

			return primaryRest(methodInvokeExpr);
		} else {
			SQLExpr value = expr();

			if (lexer.token() != SQLToken.IDENTIFIER) {
				throw new ParserException("Syntax error");
			}

			String unit = lexer.stringVal();
			lexer.nextToken();

			MySqlIntervalExpr intervalExpr = new MySqlIntervalExpr();
			intervalExpr.setValue(value);
			intervalExpr.setUnit(MySqlIntervalUnit.valueOf(unit.toUpperCase()));

			return intervalExpr;
		}
	}

	public SQLColumnDefinition parseColumn() {
		MySqlSQLColumnDefinition column = new MySqlSQLColumnDefinition();
		column.setName(name());
		column.setDataType(parseDataType());

		return parseColumnRest(column);
	}

	public SQLColumnDefinition parseColumnRest(SQLColumnDefinition column) {
		if (lexer.token() == SQLToken.ON) {
			lexer.nextToken();
			accept(SQLToken.UPDATE);
			SQLExpr expr = this.expr();
			((MySqlSQLColumnDefinition) column).setOnUpdate(expr);
		}

		if (identifierEquals("AUTO_INCREMENT")) {
			lexer.nextToken();
			if (column instanceof MySqlSQLColumnDefinition) {
				((MySqlSQLColumnDefinition) column).setAutoIncrement(true);
			}
			return parseColumnRest(column);
		}

		if (identifierEquals("precision") && column.getDataType().getName().equalsIgnoreCase("double")) {
			lexer.nextToken();
		}

		if (identifierEquals("PARTITION")) {
			throw new ParserException("syntax error " + lexer.token() + " " + lexer.stringVal());
		}

		if (identifierEquals("STORAGE")) {
			lexer.nextToken();
			SQLExpr expr = expr();
			if (column instanceof MySqlSQLColumnDefinition) {
				((MySqlSQLColumnDefinition) column).setStorage(expr);
			}
		}

		super.parseColumnRest(column);

		return column;
	}

	protected SQLDataType parseDataTypeRest(SQLDataType dataType) {
		super.parseDataTypeRest(dataType);

		if (identifierEquals("UNSIGNED")) {
			lexer.nextToken();
			dataType.getAttributes().put("UNSIGNED", true);
		}

		if (identifierEquals("ZEROFILL")) {
			lexer.nextToken();
			dataType.getAttributes().put("ZEROFILL", true);
		}

		return dataType;
	}

	public SQLExpr orRest(SQLExpr expr) {

		for (;;) {
			if (lexer.token() == SQLToken.OR || lexer.token() == SQLToken.BARBAR) {
				lexer.nextToken();
				SQLExpr rightExp = and();

				expr = new SQLBinaryOpExpr(expr, SQLBinaryOperator.BooleanOr, rightExp, JDBCConstants.MYSQL);
			} else if (lexer.token() == SQLToken.XOR) {
				lexer.nextToken();
				SQLExpr rightExp = and();

				expr = new SQLBinaryOpExpr(expr, SQLBinaryOperator.BooleanXor, rightExp, JDBCConstants.MYSQL);
			} else {
				break;
			}
		}

		return expr;
	}

	public SQLExpr additiveRest(SQLExpr expr) {
		if (lexer.token() == SQLToken.PLUS) {
			lexer.nextToken();
			SQLExpr rightExp = multiplicative();

			expr = new SQLBinaryOpExpr(expr, SQLBinaryOperator.Add, rightExp, JDBCConstants.MYSQL);
			expr = additiveRest(expr);
		} else if (lexer.token() == SQLToken.SUB) {
			lexer.nextToken();
			SQLExpr rightExp = multiplicative();

			expr = new SQLBinaryOpExpr(expr, SQLBinaryOperator.Subtract, rightExp, JDBCConstants.MYSQL);
			expr = additiveRest(expr);
		}

		return expr;
	}

	public SQLAssignItem parseAssignItem() {
		SQLAssignItem item = new SQLAssignItem();

		SQLExpr var = primary();

		String ident = null;
		if (var instanceof SQLIdentifierExpr) {
			ident = ((SQLIdentifierExpr) var).getName();

			if ("GLOBAL".equalsIgnoreCase(ident)) {
				ident = lexer.stringVal();
				lexer.nextToken();
				var = new SQLVariantRefExpr(ident, true);
			} else if ("SESSION".equalsIgnoreCase(ident)) {
				ident = lexer.stringVal();
				lexer.nextToken();
				var = new SQLVariantRefExpr(ident, false);
			} else {
				var = new SQLVariantRefExpr(ident);
			}
		}

		if ("NAMES".equalsIgnoreCase(ident)) {
			// skip
		} else if ("CHARACTER".equalsIgnoreCase(ident)) {
			var = new SQLIdentifierExpr("CHARACTER SET");
			accept(SQLToken.SET);
			if (lexer.token() == SQLToken.EQ) {
				lexer.nextToken();
			}
		} else {
			if (lexer.token() == SQLToken.COLONEQ) {
				lexer.nextToken();
			} else {
				accept(SQLToken.EQ);
			}
		}

		item.setValue(this.expr());

		item.setTarget(var);
		return item;
	}

	public SQLName nameRest(SQLName name) {
		if (lexer.token() == SQLToken.VARIANT && "@".equals(lexer.stringVal())) {
			lexer.nextToken();
			MySqlUserName userName = new MySqlUserName();
			userName.setUserName(((SQLIdentifierExpr) name).getName());

			if (lexer.token() == SQLToken.LITERAL_CHARS) {
				userName.setHost("'" + lexer.stringVal() + "'");
			} else {
				userName.setHost(lexer.stringVal());
			}
			lexer.nextToken();
			return userName;
		}
		return super.nameRest(name);
	}

	public Limit parseLimit() {
		if (lexer.token() == SQLToken.LIMIT) {
			lexer.nextToken();

			MySqlSelectQueryBlock.Limit limit = new MySqlSelectQueryBlock.Limit();

			SQLExpr temp = this.expr();
			if (lexer.token() == (SQLToken.COMMA)) {
				limit.setOffset(temp);
				lexer.nextToken();
				limit.setRowCount(this.expr());
			} else if (identifierEquals("OFFSET")) {
				limit.setRowCount(temp);
				lexer.nextToken();
				limit.setOffset(this.expr());
			} else {
				limit.setRowCount(temp);
			}
			return limit;
		}

		return null;
	}

	@Override
	public MySqlPrimaryKey parsePrimaryKey() {
		accept(SQLToken.PRIMARY);
		accept(SQLToken.KEY);

		MySqlPrimaryKey primaryKey = new MySqlPrimaryKey();

		if (identifierEquals("USING")) {
			lexer.nextToken();
			primaryKey.setIndexType(lexer.stringVal());
			lexer.nextToken();
		}

		accept(SQLToken.LPAREN);
		for (;;) {
			primaryKey.getColumns().add(this.expr());
			if (!(lexer.token() == (SQLToken.COMMA))) {
				break;
			} else {
				lexer.nextToken();
			}
		}
		accept(SQLToken.RPAREN);

		return primaryKey;
	}

	public MySqlUnique parseUnique() {
		accept(SQLToken.UNIQUE);

		if (lexer.token() == SQLToken.KEY) {
			lexer.nextToken();
		}

		if (lexer.token() == SQLToken.INDEX) {
			lexer.nextToken();
		}

		MySqlUnique unique = new MySqlUnique();

		if (lexer.token() != SQLToken.LPAREN) {
			SQLName indexName = name();
			unique.setIndexName(indexName);
		}

		accept(SQLToken.LPAREN);
		for (;;) {
			unique.getColumns().add(this.expr());
			if (!(lexer.token() == (SQLToken.COMMA))) {
				break;
			} else {
				lexer.nextToken();
			}
		}
		accept(SQLToken.RPAREN);

		if (identifierEquals("USING")) {
			lexer.nextToken();
			unique.setIndexType(lexer.stringVal());
			lexer.nextToken();
		}

		return unique;
	}

	public MysqlForeignKey parseForeignKey() {
		accept(SQLToken.FOREIGN);
		accept(SQLToken.KEY);

		MysqlForeignKey fk = new MysqlForeignKey();

		if (lexer.token() != SQLToken.LPAREN) {
			SQLName indexName = name();
			fk.setIndexName(indexName);
		}

		accept(SQLToken.LPAREN);
		this.names(fk.getReferencingColumns());
		accept(SQLToken.RPAREN);

		accept(SQLToken.REFERENCES);

		fk.setReferencedTableName(this.name());

		accept(SQLToken.LPAREN);
		this.names(fk.getReferencedColumns());
		accept(SQLToken.RPAREN);

		if (identifierEquals("MATCH")) {
			if (identifierEquals("FULL")) {
				fk.setReferenceMatch(Match.FULL);
			} else if (identifierEquals("PARTIAL")) {
				fk.setReferenceMatch(Match.PARTIAL);
			} else if (identifierEquals("SIMPLE")) {
				fk.setReferenceMatch(Match.SIMPLE);
			}
		}

		if (lexer.token() == SQLToken.ON) {
			lexer.nextToken();
			if (lexer.token() == SQLToken.DELETE) {
				fk.setReferenceOn(On.DELETE);
			} else if (lexer.token() == SQLToken.UPDATE) {
				fk.setReferenceOn(On.UPDATE);
			} else {
				throw new ParserException("syntax error, expect DELETE or UPDATE, actual " + lexer.token() + " " + lexer.stringVal());
			}
			lexer.nextToken();

			if (lexer.token() == SQLToken.RESTRICT) {
				fk.setReferenceOption(Option.RESTRICT);
			} else if (identifierEquals("CASCADE")) {
				fk.setReferenceOption(Option.CASCADE);
			} else if (lexer.token() == SQLToken.SET) {
				accept(SQLToken.NULL);
				fk.setReferenceOption(Option.SET_NULL);
			} else if (identifierEquals("ON")) {
				lexer.nextToken();
				if (identifierEquals("ACTION")) {
					fk.setReferenceOption(Option.NO_ACTION);
				} else {
					throw new ParserException("syntax error, expect ACTION, actual " + lexer.token() + " " + lexer.stringVal());
				}
			}
			lexer.nextToken();
		}
		return fk;
	}

	protected SQLAggregateExpr parseAggregateExprRest(SQLAggregateExpr aggregateExpr) {
		if (lexer.token() == SQLToken.ORDER) {
			SQLOrderBy orderBy = this.parseOrderBy();
			aggregateExpr.putAttribute("ORDER BY", orderBy);
		}
		if (identifierEquals("SEPARATOR")) {
			lexer.nextToken();

			SQLExpr seperator = this.primary();

			aggregateExpr.putAttribute("SEPARATOR", seperator);
		}
		return aggregateExpr;
	}

	public MySqlSelectGroupByExpr parseSelectGroupByItem() {
		MySqlSelectGroupByExpr item = new MySqlSelectGroupByExpr();

		item.setExpr(expr());

		if (lexer.token() == SQLToken.ASC) {
			lexer.nextToken();
			item.setType(SQLOrderingSpecification.ASC);
		} else if (lexer.token() == SQLToken.DESC) {
			lexer.nextToken();
			item.setType(SQLOrderingSpecification.DESC);
		}

		return item;
	}
}
